package com.conversationboard.model;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.mail.internet.AddressException;

import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.store.LockObtainFailedException;

import com.conversationboard.cache.IThreadCache;
import com.conversationboard.cache.RecentMessagesList;
import com.conversationboard.cache.ThreadCache;
import com.conversationboard.cache.ThreadTitleCache;
import com.conversationboard.cache.UserCache;
import com.conversationboard.cache.enums.Position;
import com.conversationboard.cache.newanswers.INewAnswersCache;
import com.conversationboard.cache.newanswers.NewAnswersCaches;
import com.conversationboard.cache.newanswers.NewAnswersThread;
import com.conversationboard.config.Configuration;
import com.conversationboard.database.Database;
import com.conversationboard.database.Queries;
import com.conversationboard.formatter.Formatter;
import com.conversationboard.formatter.IllegalMessageException;
import com.conversationboard.fulltext.index.IndexRemover;
import com.conversationboard.keygenerator.KeyGenerator;
import com.conversationboard.model.adminlog.AdminLog;
import com.conversationboard.model.exceptions.DuplicateMessageException;
import com.conversationboard.model.exceptions.PollNotPopulatedException;
import com.conversationboard.model.randomthread.RandomThread;
import com.conversationboard.site.Site;

public final class Thread {

	private int boardId;
	private int threadId;
	private String title;
	private String creatorId;
	private Date timestamp;
	private Date lastUpdated;
	private Messages messages = new Messages();
	private boolean locked;
	private boolean deleted = false;
	private boolean worksafe = true;
	private transient String ipAddress;
	private boolean poll = false;
	private Date pollClosingDate;
	private List<PollItem> pollItems = null;
	private PollResults pollResults = null;
	private boolean unanswered = false;
	private transient ConcurrentHashMap<String, String> bannedUsers = new ConcurrentHashMap<String, String>();
	private boolean deindexed;


	public boolean isDeindexed() {
		return deindexed;
	}


	public void setDeindexed(boolean deindexed) {
		this.deindexed = deindexed;
	}


	public String getLastUpdatedByDisplayName() {
		return this.messages.getLastMessage().getDisplayName();
	}


	public ConcurrentHashMap<String, String> getBannedUsers() {
		return bannedUsers;
	}


	public boolean isUnanswered() {
		return unanswered;
	}


	public void setUnanswered(boolean unanswered) {
		this.unanswered = unanswered;
	}


	public PollResults getPollResults() throws SQLException {

		/* If it's a poll, it's closed, and we've not already got the results cached, load the results up. */

		if (this.isPoll() && this.isPollClosed() && pollResults == null) {
			PollResults pollResults = new PollResults(this.getBoardId(), this.getThreadId());
			this.pollResults = pollResults;
		}

		return this.pollResults;
	}


	public List<PollItem> getPollItems() throws PollNotPopulatedException {

		if (this.isPoll() && (this.pollItems == null)) {
			throw new PollNotPopulatedException("Attempt to get poll information from a thread that is stored in the new answers cache, where it does not hold poll information. Use the Thread Cache instead.");
		}

		return pollItems;
	}


	public void setPollItems(List<PollItem> pollItems) {
		this.pollItems = pollItems;
	}


	public boolean isPollClosed() {

		if (this.pollClosingDate.before(new Date())) {
			return true;
		} else {
			return false;
		}
	}


	public Date getPollClosingDate() {
		return pollClosingDate;
	}


	public void setPollClosingDate(Date pollClosingDate) {
		this.pollClosingDate = pollClosingDate;
	}


	public boolean isPoll() {
		return poll;
	}


	public void setPoll(boolean poll) {
		this.poll = poll;
	}


	public int getMessageCount() {
		return this.messages.size();
	}


	public int getLatestMessageId() {
		
		int messageId = this.messages.get(messages.size() - 1).getMessageId(); 
		
		return messageId;
	}


	/*
	 * This is an o(n) operation, which is not good, though to date, only used for admins deleting threads. Maybe look into at some point, but it's fairly
	 * academic. If it's to be used anywhere else, be aware that it might not be fast. NOTE ALSO that the index here is not into the Java list, it's the message
	 * ID on the database, so will start at 1, rather than 0.
	 */

	public Message getMessage(int messageId) throws SQLException {

		/* Ensure that the messages are actually loaded */

		if (this.messages.isEmpty()) {
			this.getMessages();
		}

		for (Message message : this.messages) {
			if (message.getMessageId() == messageId) {
				return message;
			}
		}

		return null;
	}


	public Message getFirstMessage() throws SQLException {

		if (this.messages.isEmpty()) {
			this.getMessages();
		}

		Message message = this.messages.get(0);

		return message;
	}


	public void clearMessages() {
		this.messages = null;
	}


	public String getCreatorId() {
		return creatorId;
	}


	public void setCreatorId(String creatorId) {
		this.creatorId = creatorId;
	}


	public String getLoginId() throws SQLException {
		return this.getFirstMessage().getUserId();
	}


	public String getIpAddress() {
		return ipAddress;
	}


	public void setIpAddress(String ipAddress) {
		this.ipAddress = ipAddress;
	}


	public boolean isWorksafe() {
		return worksafe;
	}


	public void setWorksafe(boolean worksafe) {
		this.worksafe = worksafe;
	}


	public boolean isDeleted() {
		return deleted;
	}


	public void setDeleted(boolean deleted) {
		this.deleted = deleted;
	}


	public boolean isLocked() {
		return locked;
	}


	public void setLocked(boolean locked) {
		this.locked = locked;
	}


	public Date getLastUpdated() {
		return lastUpdated;
	}


	public void setLastUpdated(Date lastUpdated) {
		this.lastUpdated = lastUpdated;
	}


	public Thread(int boardId, int threadId) {
		this.boardId = boardId;
		this.threadId = threadId;
	}


	public Messages getMessages() throws SQLException {
		return this.getMessages(false);
	}


	/**
	 * Gets the messages for this thread.
	 * 
	 * @param unlessNotInCache Some operations don't want info unless it's in the cache (e.g. the count of recent messages), normally, you'll pass false here,
	 *            but occassionally, true is needed
	 * @return list of messages
	 * @throws SQLException
	 */

	public Messages getMessages(boolean unlessNotInCache) throws SQLException {

		/* 1: Look in the cache first */

		IThreadCache cache = ThreadCache.getInstance();
		Thread thread = (Thread) cache.get(this.boardId, this.threadId);

		if (thread != null) {

			this.messages = thread.messages;
			return this.messages;
		}

		if (unlessNotInCache) {
			return this.messages;
		}

		/* 2: If they're not in the cache, go and get them from the database */

		Connection connection = null;

		try {
			connection = Database.getConnection();

			/* Even if the user has requested looking at a limited amount of messages, we *never* query for this,
			 * as we want the cache to hold all the data. We can trim the contents for display purposes. */

			PreparedStatement statement = connection.prepareStatement(Queries.GET_MESSAGES_QUERY);
			statement.setInt(1, boardId);
			statement.setInt(2, threadId);

			statement.execute();

			ResultSet resultSet = statement.getResultSet();
			this.messages = new Messages();
			int arrayIndex = 0;

			while (resultSet.next()) {

				Message message = new Message(this);

				message.setMessageId(resultSet.getInt("messageid"));
				message.setBoardId(this.boardId);
				message.setThreadId(this.threadId);
				message.setTimestamp(resultSet.getTimestamp("timestamp"));
				message.setBody(resultSet.getString("body"));
				message.setDeleted(resultSet.getBoolean("deleted"));
				message.setUserId(resultSet.getString("userid"));
				message.setDisplayName(resultSet.getString("displayname"));
				message.setIpAddress(resultSet.getString("ipaddress"));
				message.setArrayIndex(arrayIndex++);

				this.messages.add(message);
			}

			statement.close();

			/* Add this thread to the cache */
			cache.put(this);

		} finally {
			Database.close(connection);
		}

		return this.messages;
	}


	/*
	 * Only to be used by classes in this package being able to add threads directly to the cache, in the event of asking a new question.
	 */

	void addMessageToLocalMessageList(Message message) {
		this.messages.add(message);
	}


	/**
	 * Insert a new message into the database, into an existing thread.
	 * 
	 * @param message the message to be inserted.
	 */

	public Message addMessage(User user, Message message, String loginId, boolean system) throws SQLException, DuplicateMessageException, IllegalMessageException, NoSuchThreadException {

		Connection connection = null;

		/* If the user is still in the sandbox, don't allow them to post */

		if ((!system) && (!user.isAbleToPost(this.getBoardId()))) {
			return null;
		}

		message.setBoardId(this.getBoardId());
		message.setThreadId(this.getThreadId());

		try {

			/* Don't let you do this if the thread is locked or deleted */
			if (this.isDeleted() || this.isLocked()) {
				return null;
			}

			/* Convert the message's contents from BBCode to HTML, but only if the board forces escapes to HTML. */

			String body = message.getBody();

			Substitutions substitutions = Site.getSubstitutions();
			body = substitutions.substitute(user, body);

			body = Formatter.escapeHtml(body);
			body = Formatter.asHtml(Boards.getBoard(this.getBoardId()), user, body, false);
			body = Formatter.mungeEmailAddr(user, body);

			message.setBody(body);

			/* Check if it's a duplicate and don't insert if it is */

			if (Configuration.getInstance().isCheckForDuplicateMessages()) {
				if (RecentMessagesList.isDuplicateMessage(message)) {
					throw new DuplicateMessageException(message.getBody());
				}
			}

			/* Now add to the database */

			connection = Database.getConnection();
			connection.setAutoCommit(false);

			PreparedStatement statement = connection.prepareStatement(Queries.INSERT_MESSAGE_QUERY);

			int messageKey = KeyGenerator.getMessageKey();

			statement.setInt(1, message.getBoardId());
			statement.setInt(2, message.getThreadId());
			statement.setInt(3, messageKey);
			statement.setString(4, message.getUserId());
			statement.setString(5, message.getDisplayName());
			statement.setString(6, message.getIpAddress());
			statement.setString(7, message.getBody());
			statement.executeUpdate();

			statement.close();

			/* Update the user's post count */

			PreparedStatement updatePostCount = connection.prepareStatement(Queries.UPDATE_POST_COUNT);
			updatePostCount.setString(1, user.getLoginId());
			updatePostCount.executeUpdate();
			updatePostCount.close();

			/* Update the post count on the cached user */

			user.incrementPostCount();

			/* Set the message's database message ID */

			message.setMessageId(messageKey);

			/* Update the new answers cache. If it's in the new answers cache, all we need to do is bump the thread. Note that you need to update the date after
			 * it's returned from the new answers cache. This ensures you get the right copy. Some tidy up of cache code is needed here. */

			INewAnswersCache cache = NewAnswersCaches.getInstance(message.getBoardId());

			/* If the cache is empty, populate it first */
			if (cache.isEmpty()) {
				Boards.getBoard(message.getBoardId()).getThreadsWithNewAnswers();
			}

			Date lastUpdated = new Date();

			/* Add the message to the cached version */

			Thread thread = Thread.get(message.getBoardId(), message.getThreadId());
			thread.getMessages().add(message);
			thread.setLastUpdated(lastUpdated);

			/* Update this new answers cache, by copying the thread object into a NewAnswersThread object and putting it (which will bump
			 * it, if it's already in there. */

			cache.putThread(thread, Position.Head);

			/* Update the site new answers cache */

			INewAnswersCache siteNewAnswers = Site.getSiteNewAnswers();
			siteNewAnswers.putThread(thread, Position.Head);

			/* Add it to the recent message list, so we can check for any duplicate messages. */

			if (Configuration.getInstance().isCheckForDuplicateMessages()) {
				RecentMessagesList.add(message);
			}

			/* Update the lastupdated timestamp on the thread to allow finding of recent threads. These two
			 * updates could ultimately be queued, if this were proving to be a bottleneck, to reduce the live
			 * updates to only inserting the message into the messages table. */

			PreparedStatement updateTimestamp = connection.prepareStatement(Queries.SET_LAST_UPDATED_TIMESTAMP_CURRENT);
			updateTimestamp.setInt(1, this.boardId);
			updateTimestamp.setInt(2, this.threadId);
			updateTimestamp.executeUpdate();
			updateTimestamp.close();

			/* If this thread now has two messages, then it is now NOT unanswered, so reflect this in the threads table */

			if (this.getMessages().size() == 2) {
				PreparedStatement setAnswered = connection.prepareStatement(Queries.SET_THREAD_ANSWERED);
				setAnswered.setInt(1, this.boardId);
				setAnswered.setInt(2, this.threadId);
				setAnswered.executeUpdate();
				setAnswered.close();

				/* Update the thread and the two new answers caches */

				thread.setUnanswered(false);
				cache.getThread(boardId, threadId).setUnanswered(false);
				siteNewAnswers.getThread(boardId, threadId).setUnanswered(false);
			}

			/* Update the latest message ID number on the thread */

			PreparedStatement updateLatestMessageIdStatement = connection.prepareStatement(Queries.UPDATE_LATEST_MESSAGE_ID);
			updateLatestMessageIdStatement.setInt(1, messageKey);
			updateLatestMessageIdStatement.setInt(2, this.boardId);
			updateLatestMessageIdStatement.setInt(3, this.threadId);

			updateLatestMessageIdStatement.executeUpdate();
			updateLatestMessageIdStatement.close();

			/* If this is a bookmarked message, then update the bookmark to your recent post */

			if (user.getBookmarks().getBookmarks().containsKey(Bookmarks.getKey(boardId, threadId))) {
				Bookmark bookmark = (Bookmark) user.getBookmarks().getBookmarks().get(Bookmarks.getKey(boardId, threadId));
				bookmark.setMessageId(message.getMessageId());
				bookmark.update(connection, user.getUserId());
			}

			connection.commit();

			/* Update the thread title cache if we succeed */
			ThreadTitleCache.setLatestMessageId(boardId, threadId, messageKey);
			Site.incrementMessageCount();

			return message;

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}
	}


	public int getThreadId() {
		return threadId;
	}


	public void setThreadId(int threadId) {
		this.threadId = threadId;
	}


	public Date getTimestamp() {
		return timestamp;
	}


	public void setTimestamp(Date timestamp) {
		this.timestamp = timestamp;
	}


	public String getTitle() {
		return title;
	}


	public void setTitle(String title) {
		this.title = title;
	}


	public int getBoardId() {
		return boardId;
	}


	public void setBoardId(int boardId) {
		this.boardId = boardId;
	}


	private static void adminActivity(String query, int boardId, int threadId, Connection connection) throws SQLException {

		boolean widerTransaction = (connection != null);

		if (!widerTransaction) {
			connection = Database.getConnection();
		}

		try {
			PreparedStatement statement = connection.prepareStatement(query);
			statement.setInt(1, boardId);
			statement.setInt(2, threadId);

			statement.executeUpdate();
			statement.close();

		} finally {
			if (!widerTransaction) {
				Database.close(connection);
			}
		}
	}


	public static void lock(User user, int boardId, int threadId) throws SQLException, NoSuchThreadException {

		Connection connection = Database.getConnection();
		connection.setAutoCommit(false);

		try {
			Thread threadToBeLocked = Thread.get(boardId, threadId);
			String title = threadToBeLocked.getTitle();

			adminActivity(Queries.ADMIN_LOCK_THREAD, boardId, threadId, connection);

			/* Update the cached versions */

			IThreadCache threadCache = ThreadCache.getInstance();
			Thread cachedThread = (Thread) threadCache.get(boardId, threadId);

			if (cachedThread != null) {
				cachedThread.setLocked(true);
			}

			INewAnswersCache newAnswersCache = NewAnswersCaches.getInstance(boardId);
			NewAnswersThread thread = newAnswersCache.getThread(boardId, threadId);

			if (thread != null) {
				thread.setLocked(true);
			}

			INewAnswersCache siteNewAnswersCache = Site.getSiteNewAnswers();
			NewAnswersThread siteThread = siteNewAnswersCache.getThread(boardId, threadId);

			if (siteThread != null) {
				siteThread.setLocked(true);
			}

			AdminLog.getInstance().write(connection, user, boardId, "Thread: <strong>" + title + "</strong> has been locked.");

			connection.commit();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}

	}


	public static void unlock(User user, int boardId, int threadId) throws SQLException, NoSuchThreadException {

		Connection connection = Database.getConnection();
		connection.setAutoCommit(false);

		try {
			Thread threadToBeUnlocked = Thread.get(boardId, threadId);
			String title = threadToBeUnlocked.getTitle();

			adminActivity(Queries.ADMIN_UNLOCK_THREAD, boardId, threadId, null);

			/* Update the cached versions */

			IThreadCache threadCache = ThreadCache.getInstance();
			Thread cachedThread = (Thread) threadCache.get(boardId, threadId);

			if (cachedThread != null) {
				cachedThread.setLocked(false);
			}

			INewAnswersCache newAnswersCache = NewAnswersCaches.getInstance(boardId);
			NewAnswersThread thread = newAnswersCache.getThread(boardId, threadId);

			if (thread != null) {
				thread.setLocked(false);
			}

			INewAnswersCache siteNewAnswersCache = Site.getSiteNewAnswers();
			NewAnswersThread siteThread = siteNewAnswersCache.getThread(boardId, threadId);

			if (siteThread != null) {
				siteThread.setLocked(false);
			}

			AdminLog.getInstance().write(connection, user, boardId, "Thread: <strong>" + title + "</strong> has been unlocked.");

			connection.commit();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}

	}


	public static void delete(User user, int boardId, int threadId, Connection connection) throws SQLException, NoSuchThreadException {

		Thread threadToBeDeleted = Thread.get(boardId, threadId);
		String title = threadToBeDeleted.getTitle();

		/* Delete from disk */

		adminActivity(Queries.ADMIN_DELETE_THREAD, boardId, threadId, connection);
		adminActivity(Queries.ADMIN_DELETE_MESSAGES, boardId, threadId, connection);

		/* Update the cached versions */

		IThreadCache threadCache = ThreadCache.getInstance();
		Thread cachedThread = (Thread) threadCache.get(boardId, threadId);

		if (cachedThread != null) {
			cachedThread.setDeleted(true);
		}

		INewAnswersCache newAnswersCache = NewAnswersCaches.getInstance(boardId);
		NewAnswersThread thread = newAnswersCache.getThread(boardId, threadId);

		if (thread != null) {
			thread.setDeleted(true);
		}

		INewAnswersCache siteNewAnswersCache = Site.getSiteNewAnswers();
		NewAnswersThread siteThread = siteNewAnswersCache.getThread(boardId, threadId);

		if (siteThread != null) {
			siteThread.setDeleted(true);
		}

		AdminLog.getInstance().write(connection, user, boardId, "Thread: <strong>" + title + "</strong> deleted.");
	}


	public static void undelete(User user, int boardId, int threadId, Connection connection) throws SQLException, NoSuchThreadException {

		Thread threadToBeUndeleted = Thread.get(boardId, threadId);
		String title = threadToBeUndeleted.getTitle();

		adminActivity(Queries.ADMIN_UNDELETE_THREAD, boardId, threadId, connection);
		adminActivity(Queries.ADMIN_UNDELETE_MESSAGES, boardId, threadId, connection);

		/* Update the cached versions */

		IThreadCache threadCache = ThreadCache.getInstance();
		Thread cachedThread = (Thread) threadCache.get(boardId, threadId);

		if (cachedThread != null) {
			cachedThread.setDeleted(false);
		}

		INewAnswersCache newAnswersCache = NewAnswersCaches.getInstance(boardId);
		NewAnswersThread thread = newAnswersCache.getThread(boardId, threadId);

		if (thread != null) {
			thread.setDeleted(false);
		}

		INewAnswersCache siteNewAnswersCache = Site.getSiteNewAnswers();
		NewAnswersThread siteThread = siteNewAnswersCache.getThread(boardId, threadId);

		if (siteThread != null) {
			siteThread.setDeleted(false);
		}

		AdminLog.getInstance().write(connection, user, boardId, "Thread: <strong>" + title + "</strong> undeleted.");

	}


	public static void makeWorksafe(int boardId, int threadId) throws SQLException {

		adminActivity(Queries.ADMIN_MAKE_THREAD_WORKSAFE, boardId, threadId, null);

		/* Update the cached versions */

		IThreadCache threadCache = ThreadCache.getInstance();
		Thread cachedThread = (Thread) threadCache.get(boardId, threadId);

		if (cachedThread != null) {
			cachedThread.setWorksafe(true);
		}

		INewAnswersCache newAnswersCache = NewAnswersCaches.getInstance(boardId);
		NewAnswersThread thread = newAnswersCache.getThread(boardId, threadId);

		if (thread != null) {
			thread.setWorksafe(true);
		}

		INewAnswersCache siteNewAnswersCache = Site.getSiteNewAnswers();
		NewAnswersThread siteThread = siteNewAnswersCache.getThread(boardId, threadId);

		if (siteThread != null) {
			siteThread.setWorksafe(true);
		}

	}


	public static void makeWorkUnsafe(int boardId, int threadId) throws SQLException {

		adminActivity(Queries.ADMIN_MAKE_THREAD_WORKUNSAFE, boardId, threadId, null);

		/* Update the cached versions */

		IThreadCache threadCache = ThreadCache.getInstance();
		Thread cachedThread = (Thread) threadCache.get(boardId, threadId);

		if (cachedThread != null) {
			cachedThread.setWorksafe(false);
		}

		INewAnswersCache newAnswersCache = NewAnswersCaches.getInstance(boardId);
		NewAnswersThread thread = newAnswersCache.getThread(boardId, threadId);

		if (thread != null) {
			thread.setWorksafe(false);
		}

		INewAnswersCache siteNewAnswersCache = Site.getSiteNewAnswers();
		NewAnswersThread siteThread = siteNewAnswersCache.getThread(boardId, threadId);

		if (siteThread != null) {
			siteThread.setWorksafe(false);
		}

	}


	public void index() throws SQLException {

		Connection connection = null;

		try {
			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.INDEX_THREAD);
			statement.setInt(1, this.getBoardId());
			statement.setInt(2, this.getThreadId());

			statement.executeUpdate();

			statement.close();

			this.setDeindexed(false);

		} finally {
			Database.close(connection);
		}

	}


	public void deIndex() throws SQLException {

		Connection connection = null;

		try {
			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.DEINDEX_THREAD);
			statement.setInt(1, this.getBoardId());
			statement.setInt(2, this.getThreadId());

			statement.executeUpdate();

			statement.close();

			this.setDeindexed(true);

		} finally {
			Database.close(connection);
		}

	}


	public void updateTitle(User user, String title) throws SQLException {

		Connection connection = Database.getConnection();
		connection.setAutoCommit(false);

		String originalTitle = this.getTitle();

		try {

			/* Update the database */

			PreparedStatement statement = connection.prepareStatement(Queries.ADMIN_UPDATE_THREAD_TITLE);

			statement.setString(1, title);
			statement.setInt(2, this.getBoardId());
			statement.setInt(3, this.getThreadId());

			statement.executeUpdate();

			statement.close();

			/* Now the cached versions (this and the new answers cache) */

			this.setTitle(title);

			INewAnswersCache cache = NewAnswersCaches.getInstance(this.getBoardId());
			NewAnswersThread thread = cache.getThread(boardId, threadId);
			thread.setTitle(title);

			INewAnswersCache siteCache = Site.getSiteNewAnswers();
			NewAnswersThread siteThread = siteCache.getThread(boardId, threadId);

			if (siteThread != null) {
				siteThread.setTitle(title);
			}

			/* Also update the thread title cache with this change, or the change will only be picked up on restart. */
			
			/* Odd - updating this makes the bookmarks section not show the bookmark. Need to investigate. */
//			ThreadTitleCache.putTitle(boardId, threadId, title);

			AdminLog.getInstance().write(connection, user, boardId, "<strong>Thread:</strong> " + originalTitle + " <strong>had its title updated to</strong>: " + title);

			connection.commit();

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}
	}


	/**
	 * Move a thread from one board to another. Simply by updating the thread, message and possible poll's board IDs to be
	 * that of the target board ID.
	 *
	 * We also have to deindex the thread from the index and make sure the thread is reindexed.
	 * 
	 * @param targetBoardId the board to which you want to move this thread to
	 * @return the number of messages updated (not really useful)
	 * @throws SQLException
	 * @throws IOException 
	 * @throws LockObtainFailedException 
	 * @throws CorruptIndexException 
	 */

	public void moveToBoard(User user, int targetBoardId) throws SQLException, AddressException, IllegalMessageException, CorruptIndexException, LockObtainFailedException, IOException, NoSuchThreadException {

		/* Update the thread, the messages and any associated poll information to have the target board ID */

		Connection connection = Database.getConnection();
		connection.setAutoCommit(false);

		Board board = Boards.getBoard(targetBoardId);

		try {

			/* Update the thread to have the new board ID. Get a new thread ID and assign that. */

			int targetThreadId = KeyGenerator.getThreadKey();

			PreparedStatement statement = connection.prepareStatement(Queries.MOVE_THREAD_TO_NEW_BOARD);
			statement.setInt(1, targetBoardId);
			statement.setInt(2, targetThreadId);
			statement.setInt(3, this.getBoardId());
			statement.setInt(4, this.getThreadId());
			statement.executeUpdate();

			statement.close();

			/* Update the messages to have the new board ID */

			statement = connection.prepareStatement(Queries.MOVE_THREAD_MESSAGES_TO_NEW_BOARD);
			statement.setInt(1, targetBoardId);
			statement.setInt(2, targetThreadId);
			statement.setInt(3, this.getBoardId());
			statement.setInt(4, this.getThreadId());
			statement.executeUpdate();

			statement.close();

			/* If it's a poll, update the poll information too */

			if (this.isPoll()) {
				statement = connection.prepareStatement(Queries.MOVE_POLL_ITEMS_TO_NEW_BOARD);
				statement.setInt(1, targetBoardId);
				statement.setInt(2, targetThreadId);
				statement.setInt(3, this.getBoardId());
				statement.setInt(4, this.getThreadId());
				statement.executeUpdate();

				statement.close();
			}

			/* TODO: Update the voter cache too! */

			/* Update any bookmarks that are pointing at the old thread on disk */

			statement = connection.prepareStatement(Queries.UPDATE_MOVED_THREAD_BOOKMARKS);
			statement.setInt(1, targetBoardId);
			statement.setInt(2, targetThreadId);
			statement.setInt(3, this.getBoardId());
			statement.setInt(4, this.getThreadId());
			statement.executeUpdate();

			statement.close();

			/* Log the action */

			String message = "Moved thread: <strong>" + this.getTitle() + "</strong> from board: <strong>" + this.getBoardName() + "</strong> to board: <strong>" + board.getName() + "</strong>";
			AdminLog.getInstance().write(connection, user, 0, message);

			connection.commit();

			/* Update the indexed copy with the new Board ID and Thread ID */

			IndexRemover.removeThread(this.getBoardId(), this.getThreadId());

			/* Remove from Thread Cache */

			IThreadCache threadCache = ThreadCache.getInstance();
			threadCache.remove(boardId, threadId);

			/* Remove from source's new answers cache */

			INewAnswersCache sourceNewAnswersCache = NewAnswersCaches.getInstance(boardId);
			sourceNewAnswersCache.removeThread(boardId, threadId);

			/* Clear and reload the target board's new answers cache, now that changes are committed.
			 * (This could be optimised if I write something to allow me to insert it at the right place). */

			INewAnswersCache targetNewAnswersCache = NewAnswersCaches.getInstance(targetBoardId);
			targetNewAnswersCache.clear();
			board.getThreadsWithNewAnswers(true);

			/* Update site new answers of the moved thread too, if it's in site new answers. */

			INewAnswersCache siteCache = Site.getSiteNewAnswers();
			NewAnswersThread siteThread = siteCache.getThread(boardId, threadId);
			siteCache.removeThread(boardId, threadId);

			if (siteThread != null) {
				siteThread.setBoardId(targetBoardId);
				siteThread.setThreadId(targetThreadId);
			}

			/* Reload the site new answers cache from scratch */
			Site.initialiseSiteNewAnswersCache();

			/* Update it in the thread title cache */

			ThreadTitleCache.moveThread(boardId, threadId, targetBoardId, targetThreadId);

			/* Update the cached versions of bookmarks in the users' cache to reflect the new postions */

			UserCache.getInstance().updateBookmarks(boardId, threadId, targetBoardId, targetThreadId);

		} catch (SQLException e) {
			connection.rollback();
			throw e;
		} finally {
			Database.close(connection);
		}

	}


	/**
	 * Delete the passed in list of messages from this thread; database and cache
	 * 
	 * @param messageIDs the list of messages to be removed.
	 * @throws SQLException
	 */

	public void deleteMessages(User user, List<Integer> messageIDs) throws SQLException, NoSuchThreadException {

		/* Convert to a map of integers (so as we can find messages in o(1)) */

		Map<Integer, Integer> messageIDsToBeDeleted = new HashMap<Integer, Integer>(messageIDs.size() * 3);

		for (int messageId : messageIDs) {
			messageIDsToBeDeleted.put(messageId, messageId);
		}

		Connection connection = Database.getConnection();

		try {

			/* Delete them from the database */

			PreparedStatement statement = connection.prepareStatement(Queries.ADMIN_DELETE_MESSAGE);

			for (int messageId : messageIDsToBeDeleted.values()) {
				statement.setInt(1, this.boardId);
				statement.setInt(2, this.threadId);
				statement.setInt(3, messageId);
				statement.addBatch();
			}

			statement.executeBatch();
			statement.close();

			/* Now delete them from the cached version */

			for (Message message : this.getMessages()) {

				int messageId = message.getMessageId();

				if (messageIDsToBeDeleted.containsKey(messageId)) {
					message.setDeleted(true);
				}
			}

			/* Record on the admin log */

			Thread thread = Thread.get(boardId, threadId);

			StringBuilder buffer = new StringBuilder();
			buffer.append("Multiple messages: [");

			for (int messageId : messageIDsToBeDeleted.values()) {
				buffer.append(messageId + ", ");
			}

			buffer = new StringBuilder(buffer.substring(0, buffer.length() - 2));
			buffer.append("] deleted from thread: <strong>" + thread.getTitle() + "</strong>");

			AdminLog.getInstance().write(connection, user, this.getBoardId(), buffer.toString());

		} finally {
			Database.close(connection);
		}
	}


	public void populatePoll() throws SQLException {

		Connection connection = Database.getConnection();

		try {
			PreparedStatement statement = connection.prepareStatement(Queries.GET_POLL_OPTIONS);
			statement.setInt(1, this.boardId);
			statement.setInt(2, this.getThreadId());
			statement.execute();

			ResultSet resultSet = statement.getResultSet();

			List<PollItem> pollItems = new LinkedList<PollItem>();

			while (resultSet.next()) {
				PollItem pollItem = new PollItem();
				pollItem.setOptionId(resultSet.getInt("optionid"));
				pollItem.setOptionText(resultSet.getString("optiontext"));
				pollItems.add(pollItem);
			}

			statement.close();
			this.setPollItems(pollItems);

		} finally {
			Database.close(connection);
		}

	}


	public static Thread get(int boardId, int threadId) throws SQLException, NoSuchThreadException {

		/* Look in cache for this thread. If we don't find it, then get it from the database */

		IThreadCache threadCache = ThreadCache.getInstance();
		Thread thread = threadCache.get(boardId, threadId);

		/* It wasn't in the cache, get it in the database */

		if (thread == null) {

			Site.incrementCacheMissCount();

			thread = new Thread(boardId, threadId);

			Connection connection = Database.getConnection();

			try {

				PreparedStatement statement = connection.prepareStatement(Queries.GET_THREAD);
				statement.setInt(1, boardId);
				statement.setInt(2, threadId);

				statement.execute();

				ResultSet resultSet = statement.getResultSet();

				int count = 0;

				while (resultSet.next()) {
					thread.setTimestamp(resultSet.getTimestamp("timestamp"));
					thread.setLastUpdated(resultSet.getTimestamp("lastupdated"));
					thread.setTitle(resultSet.getString("title"));
					thread.setDeleted(resultSet.getBoolean("deleted"));
					thread.setLocked(resultSet.getBoolean("locked"));
					thread.setWorksafe(resultSet.getBoolean("worksafe"));
					thread.setUnanswered(resultSet.getBoolean("unanswered"));
					thread.setPoll(resultSet.getBoolean("poll"));
					thread.setPollClosingDate(resultSet.getDate("pollclosingdate"));
					thread.setDeindexed(resultSet.getBoolean("deindexed"));

					count++;
				}

				resultSet.close();
				statement.close();

				if (count == 0) {
					throw new NoSuchThreadException("No such thread [board ID = " + boardId + ", thread ID = " + threadId + "]");
				}

				/* If the thread is a poll, get and set up the poll items */

				if (thread.isPoll()) {
					thread.populatePoll();
				}

				/* Get the thread's messages and place it in the message cache */
				thread.getMessages();

				if (thread.getMessageCount() == 0) {
					throw new IllegalStateException("Inconsistent thread (no messages): " + thread.getBoardId() + "/" + thread.getThreadId());
				}

				thread.setCreatorId(thread.getFirstMessage().getDisplayName());

				threadCache.put(thread);

				/* If it's in the new answers cache, then we can update the number of new answers */

				INewAnswersCache newAnswersCache = NewAnswersCaches.getInstance(boardId);
				NewAnswersThread newAnswersThread = newAnswersCache.getThread(boardId, threadId);

				if (newAnswersThread != null) {
					newAnswersThread.setNumberOfRecentMessages(thread.getMessages().getNumberOfRecentMessages());
					newAnswersThread.setLastUpdatedByDisplayName(thread.getLastUpdatedByDisplayName());
				}

				/* Similarly the Site New Answers Cache */

				INewAnswersCache siteNewAnswersCache = Site.getSiteNewAnswers();
				NewAnswersThread siteNewAnswersThread = siteNewAnswersCache.getThread(boardId, threadId);

				if (siteNewAnswersThread != null) {
					siteNewAnswersThread.setNumberOfRecentMessages(thread.getMessages().getNumberOfRecentMessages());
					siteNewAnswersThread.setLastUpdatedByDisplayName(thread.getLastUpdatedByDisplayName());
				}

			} finally {
				Database.close(connection);
			}

		} else {
			Site.incrementCacheHitCount();
		}

		return thread;
	}


	public String getBoardName() throws SQLException {
		return Boards.getBoard(this.boardId).getName();
	}


	public static Thread getRandomThread(User user) throws SQLException, NoSuchThreadException {
		return ThreadTitleCache.getRandomThread(user);
	}


	public static List<RandomThread> getRandomThreads(User user, int number) throws SQLException {
		return ThreadTitleCache.getRandomThreads(user, number);
	}

}
